package org.vaadinpa.gui;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.UUID;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.vaadin.artur.icepush.ICEPush;
import org.vaadinpa.Conf;
import org.vaadinpa.data.Coordinate;
import org.vaadinpa.data.Planet;
import org.vaadinpa.data.Universe;
import org.vaadinpa.gui.util.NotificationUtil;
import org.vaadinpa.logic.TickListener;
import org.vaadinpa.logic.Ticker;

import com.vaadin.Application;
import com.vaadin.service.ApplicationContext.TransactionListener;
import com.vaadin.terminal.gwt.server.HttpServletRequestListener;
import com.vaadin.ui.Window;
import com.vaadin.ui.themes.Reindeer;

@SuppressWarnings("serial")
public class VaadinPA extends Application implements TransactionListener, HttpServletRequestListener, TickListener {

	public static final String SERVER_INSTANCE_ID = UUID.randomUUID().toString();
	private static final String COOKIE_NAME = "vaadinPA_planet";
	private static final String COOKIE_SEPARATOR = "_";
	private static final String COOKIE_SALT = "I can has cheezburger?";
	private static final int COOKIE_INDEX_SERVER = 0;
	private static final int COOKIE_INDEX_X = 1;
	private static final int COOKIE_INDEX_Y = 2;
	private static final int COOKIE_INDEX_SIGNATURE = 3;

	private static List<Coordinate> unusedCoords;

	private static ThreadLocal<Planet> planetForThis = new ThreadLocal<Planet>();
	private static ThreadLocal<VaadinPA> thisApp = new ThreadLocal<VaadinPA>();

	private ICEPush pusher;
	private String coordsFromCookie;
	private String coordsToCookie;
	private Window mainWindow;

	@Override
	public void init() {

		setTheme("vaadinpatheme");

		setCurrent(this);

		getContext().addTransactionListener(this);

		// determine player
		Planet current = getPlanetFromCookie();

		mainWindow = new Window("VaadinPA");
		mainWindow.setStyleName(Reindeer.WINDOW_BLACK);
		mainWindow.setSizeFull();
		if (current == null) {
			// new player
			mainWindow.setContent(new CreatePlayerLayout());

		} else {
			Planet p = getPlanetFromCookie();
			setPlanet(p);
			setUser(p);
			showMainLayout();
		}

		setMainWindow(mainWindow);
	}

	/**
	 * Creates a new Planet to the system.<br>
	 */
	public synchronized void createNewPlayer(String rulerName, String planetName) {

		for (Planet p : Universe.instance.values()) {
			if (p.ruler.equals(rulerName) || p.name.equals(planetName)) {
				throw new IllegalArgumentException();
			}
		}

		Coordinate coords = getRandomUnusedCoordinates();

		Planet planet = new Planet(coords);
		planet.addBasicResources();
		planet.ruler = rulerName;
		planet.name = planetName;

		Universe.instance.put(coords, planet);
		setUser(planet);
		setPlanet(planet);

		// update cookie
		String cookieval = getCookieString(coords);
		// add signature
		cookieval += COOKIE_SEPARATOR + SHA1(cookieval + COOKIE_SALT);
		coordsToCookie = cookieval;
	}

	public void showMainLayout() {
		mainWindow.setContent(new MainLayout());

		if (pusher == null) {
			pusher = new ICEPush();
		}
		mainWindow.getContent().addComponent(pusher);

		Ticker.addListener(this);
	}

	private static String convertToHex(byte[] data) {
		StringBuffer buf = new StringBuffer();
		for (int i = 0; i < data.length; i++) {
			int halfbyte = data[i] >>> 4 & 0x0F;
			int two_halfs = 0;
			do {
				if (0 <= halfbyte && halfbyte <= 9) {
					buf.append((char) ('0' + halfbyte));
				} else {
					buf.append((char) ('a' + (halfbyte - 10)));
				}
				halfbyte = data[i] & 0x0F;
			} while (two_halfs++ < 1);
		}
		return buf.toString();
	}

	private static String SHA1(String text) {
		try {
			MessageDigest md = MessageDigest.getInstance("SHA-1");
			byte[] sha1hash = new byte[40];
			md.update(text.getBytes("iso-8859-1"), 0, text.length());
			sha1hash = md.digest();
			return convertToHex(sha1hash);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return "";
	}

	private Planet getPlanetFromCookie() {
		if (coordsFromCookie == null) {
			return null;
		}

		String[] coordstrings = coordsFromCookie.split(COOKIE_SEPARATOR);

		String serverId;
		int x;
		int y;
		String signature;
		try {
			serverId = coordstrings[COOKIE_INDEX_SERVER];
			x = Integer.parseInt(coordstrings[COOKIE_INDEX_X]);
			y = Integer.parseInt(coordstrings[COOKIE_INDEX_Y]);
			signature = coordstrings[COOKIE_INDEX_SIGNATURE];
		} catch (Exception e) {
			// cookie is in wrong format
			return null;
		}

		if (!serverId.equals(SERVER_INSTANCE_ID)) {
			// cookie is old
			return null;
		}

		Coordinate coords = new Coordinate(x, y);

		// test validity
		String gottenCookieVal = getCookieString(coords);
		if (!signature.equals(SHA1(gottenCookieVal + COOKIE_SALT))) {
			// invalid cookie
			System.err.println("Modified cookie gotten");
			return null;
		}

		Planet planet = Universe.instance.get(coords);
		if (planet == null) {
			System.err.println("Invalid cookie; coords are not in play.");
		}
		return planet;
	}

	private static String getCookieString(Coordinate coords) {
		return SERVER_INSTANCE_ID + COOKIE_SEPARATOR + coords.x + COOKIE_SEPARATOR + coords.y;
	}

	private Coordinate getRandomUnusedCoordinates() {

		if (unusedCoords == null) {
			// create universe
			unusedCoords = new LinkedList<Coordinate>();
			for (int i = 0; i < Conf.UNIVERSE_SIZE; i++) {
				for (int j = 0; j < Conf.UNIVERSE_SIZE; j++) {
					unusedCoords.add(new Coordinate(i, j));
				}
			}
		}

		if (unusedCoords.size() == 0) {
			// universe size is static, no more players allowed
			return null;
		}

		Random r = new Random();
		final int index = r.nextInt(unusedCoords.size());
		Coordinate coords = unusedCoords.get(index);
		unusedCoords.remove(index);

		return coords;
	}

	public static void push() {
		getCurrent().pusher.push();
	}

	public static void setCurrent(VaadinPA thisApp) {
		VaadinPA.thisApp.set(thisApp);
	}

	public static VaadinPA getCurrent() {
		return thisApp.get();
	}

	public static void setPlanet(Planet planet) {
		planetForThis.set(planet);
	}

	public static Planet getPlanet() {
		return planetForThis.get();
	}

	private void setThreadLocals() {
		setPlanet((Planet) getUser());
		setCurrent(this);
	}

	public void transactionStart(final Application application, final Object transactionData) {
		// Same WebApplicationContext (session) may contain multiple different
		// Vaadin applications, here we are only interested of
		// VaadinPA related transaction events.
		if (application == VaadinPA.this) {
			setThreadLocals();
		}

		if (!application.isRunning()) {
			return;
		}

	}

	public void transactionEnd(final Application application, final Object transactionData) {
		setPlanet(null);
		setCurrent(null);
	}

	@Override
	public void close() {
		Ticker.removeListener(this);
		super.close();
	}

	public void onRequestEnd(final HttpServletRequest request, final HttpServletResponse response) {
	}

	public void onRequestStart(final HttpServletRequest request, final HttpServletResponse response) {

		// send new player coords to browser
		if (coordsToCookie != null) {
			final Cookie cookie = new Cookie(COOKIE_NAME, coordsToCookie.toString());
			cookie.setPath("/");
			cookie.setMaxAge(60 * 60 * 24 * 365);// 1 year
			response.addCookie(cookie);
			coordsFromCookie = coordsToCookie.toString();
			coordsToCookie = null;

			System.out.println("Writing locale cookie for new user, value=" + cookie.getValue());
		}

		// read player coords once from browser
		if (coordsFromCookie == null) {
			final Cookie[] cookies = request.getCookies();
			if (cookies != null) {
				for (final Cookie cookie : cookies) {
					if (cookie.getName().equals(COOKIE_NAME)) {
						coordsFromCookie = cookie.getValue();
					}
				}
			}
		}
	}

	private void refreshAll() {
		if (getMainWindow().getContent() instanceof MainLayout) {
			setThreadLocals();
			((MainLayout) getMainWindow().getContent()).refresh();
		}
	}

	public void tickOccurred() {
		refreshAll();
		push();
	}

	@Override
	public void ticksStarted() {
		refreshAll();
		if (getMainWindow().getContent() instanceof MainLayout) {
			NotificationUtil.notify("Ticks started", "");
		}
		push();
	}

	@Override
	public void ticksEnded() {
		refreshAll();
		if (getMainWindow().getContent() instanceof MainLayout) {
			NotificationUtil.notify("Ticks stopped", "");
		}
		push();
	}

	@Override
	public void ticksStartScheduled() {
		refreshAll();
		push();
	}

}
